Module modules.resources
File name: resources.py Author: Martin Jůda Python Version: 3.7 Description: Module with REST API resources
Expand source code
"""
File name: resources.py
Author: Martin Jůda
Python Version: 3.7
Description: Module with REST API resources
"""
import base64
import random
import re
from datetime import datetime
from flask_httpauth import HTTPBasicAuth
from flask_restful import Resource, reqparse, abort, HTTPException
from sqlalchemy.exc import OperationalError
from modules.constants import (
DEFAULT_NUMBER_OF_QUESTIONS,
CREDENTIALS,
MAX_NUMBER_OF_QUESTIONS,
DEFAULT_PERMISSION,
)
from modules.db import Database
from modules.errors import NoActiveStudyError, StagUnavailableError
from modules.logger import logger
from modules.stag import StagCommunicator
def handle_stag_unavailable_error():
"""Raise HTTP 503 - Internal server error"""
abort(503, message="Request to STAG API failed.")
def internal_server_error():
"""Raise HTTP 500 - Internal server error"""
abort(500, message="Internal server error.")
auth = HTTPBasicAuth()
@auth.verify_password
def verify(username, password):
"""Verify incoming request credentials
Args:
username (str): user's login
password (str): user's password
Returns:
bool: True if user and password are ok otherwise False
"""
if not (username and password):
return False
return CREDENTIALS.get(username) == password
class Question(Resource):
@auth.login_required
def get(self):
"""GET /questions HTTP method"""
try:
args = self._validate_request_params()
db_session = None
try:
db = Database()
db_session = db.get_db_session()
except OperationalError:
logger.exception(msg="Problem with connection to database")
self._close_db_session(db_session)
abort(500, message="Can not connect to database.")
device = db.device_by_id(db_session, args["device_id"])
if not device:
abort(400, message="This device id is not registered in database")
# Get assigned study programmes to a device
device_permissions = [d.study_code for d in device.study_programmes]
db.store_access_log(db_session, args["card_number"], args["device_id"])
stag = StagCommunicator()
try:
studies = stag.get_active_studies(card_number=args["card_number"])
study_codes = stag.get_study_codes_for_active_studies(studies)
self.evaluate_user_device_permission(
reference_codes=device_permissions, study_codes=study_codes
)
# Get the subjects for the first active study
completed_subjects = stag.get_completed_subjects_for_study(
study=studies[0], actual_year=datetime.today().year
)
except NoActiveStudyError:
logger.info(f"No active study for card_number: {args['card_number']}")
self._close_db_session(db_session)
abort(
404, message="No active study found.", error_code="NO_ACTIVE_STUDY"
)
except StagUnavailableError:
logger.exception(msg="Can not connect establish connection to STAG.")
self._close_db_session(db_session)
handle_stag_unavailable_error()
number_of_questions = ( # set requested number of questions
args.get("question_count")
if args.get("question_count")
else DEFAULT_NUMBER_OF_QUESTIONS
)
subjects = self._choose_subjects(completed_subjects, number_of_questions)
questions = db.select_questions(
db_session=db_session,
subjects=subjects,
requested_number_of_questions=number_of_questions,
)
questions = [self._question_to_dict(q) for q in questions]
self._close_db_session(db_session)
return {
"questions": questions,
"student_name": self._student_name(studies[0]),
}
except HTTPException:
raise
except Exception as err:
logger.exception(err)
internal_server_error()
def _validate_request_params(self):
"""HTTP params parser and validator
Returns:
dict: Incoming request parameters
Raises:
HTTPException: HTTP 400 bad requests on invalid params
"""
arg_parser = reqparse.RequestParser()
arg_parser.add_argument(
"card_number",
type=str,
required=True,
location="args",
)
arg_parser.add_argument("device_id", type=int, required=True, location="args")
arg_parser.add_argument(
"question_count", type=int, required=False, location="args"
)
args = arg_parser.parse_args(strict=True)
self._validate_card_number(card_number=args["card_number"])
self._validate_device_id(device_id=args["device_id"])
self._validate_questions_count(question_count=args.get("question_count"))
return args
def evaluate_user_device_permission(self, reference_codes, study_codes):
"""Evaluate if user has access to this device
Args:
reference_codes list(str): list of allowed codes
study_codes list(str): list of student codes
Raises:
HTTPException: HTTP 403 Forbidden - access not allowed
"""
if not self._is_study_code_in_reference(study_codes, reference_codes):
abort(403, message="This student does not have access to this device")
@staticmethod
def _is_study_code_in_reference(study_codes, reference_codes):
"""Test if some of student codes is in reference
Args:
reference_codes list(str): list of allowed codes
study_codes list(str): list of student codes
Returns:
True if reference contains some student code otherwise False
"""
if DEFAULT_PERMISSION in reference_codes:
return True
for study_code in study_codes:
if study_code in reference_codes:
return True
return False
@staticmethod
def _student_name(study_info):
"""Format student name based on info from Stag
Args:
study_info (dict): Info about student from Stag
Returns:
str: student's name
"""
if study_info["titulPred"]:
name = f"{study_info['titulPred']} "
else:
name = ""
name += f"{(study_info['jmeno']).upper()} {(study_info['prijmeni']).upper()}"
if study_info["titulZa"]:
name += f", {study_info['titulZa']}"
return name
@staticmethod
def _close_db_session(db_session):
"""Close database connection"""
if db_session:
db_session.close()
@staticmethod
def _choose_subjects(completed_subjects, number_of_questions):
"""Choose random subjects from completed study
Args:
completed_subjects (list): student's absolved subjects
number_of_questions (int): number of subjects to choose
Returns:
list: choose subjects
"""
if not completed_subjects:
return []
if len(completed_subjects) < number_of_questions:
return completed_subjects
return random.sample(completed_subjects, number_of_questions)
@staticmethod
def _validate_card_number(card_number):
"""Test if card number matches expected format
Args:
card_number (str): Student's card number
Raises:
HTTPException: HTTP 400 Bad request on invalid card number
"""
if not re.match(r"^[A-Z0-9]{8}$", card_number):
abort(400, message="Invalid card_number format.")
@staticmethod
def _validate_questions_count(question_count):
"""Test if requested number of questions is in pre-set range
Args:
question_count (int): number of questions requested by client
Raises:
HTTPException: HTTP 400 Bad request on invalid number
"""
if question_count is not None:
if question_count <= 0 or question_count > MAX_NUMBER_OF_QUESTIONS:
abort(
400,
message=f"Invalid question_count. Value must be in "
f"range 1-{MAX_NUMBER_OF_QUESTIONS}.",
)
@staticmethod
def _validate_device_id(device_id):
"""Test if device id is not negative
Args:
device_id (int): id of device
Raises:
HTTPException: HTTP 400 Bad request on invalid number
"""
if device_id < 1:
abort(400, message=f"Invalid device_id. Value must be positive.")
@staticmethod
def _question_to_dict(question_entity):
"""Transform question to output format
Args:
question_entity: DB question class entity
Returns:
dict: Response (json) format of question
"""
return {
"question": question_entity.question,
"department_shortcut": question_entity.department_shortcut,
"subject_name": question_entity.subject.name,
"right_answer": question_entity.right_answer,
"wrong_answer_1": question_entity.wrong_answer_1,
"wrong_answer_2": question_entity.wrong_answer_2,
"wrong_answer_3": question_entity.wrong_answer_3,
"picture_id": question_entity.id if question_entity.picture else None,
}
class Image(Resource):
@auth.login_required
def get(self, image_id):
"""GET /images/<image_id> HTTP method
Args:
image_id (int): ID reference of question's picture
"""
try:
db_session = None
try:
db = Database()
db_session = db.get_db_session()
except OperationalError:
logger.exception(msg="Problem with connection to database")
self._close_db_session(db_session)
abort(500, message="Can not connect to database.")
try:
image = db.find_question_image(db_session, image_id)
except LookupError:
self._close_db_session(db_session)
abort(404, message="Invalid image ID.")
if not image.picture:
abort(404, message="This question does not have image.")
self._close_db_session(db_session)
return {"image": (base64.b64encode(image.picture)).decode("utf-8")}
except HTTPException:
raise
except Exception as err:
logger.exception(err)
raise internal_server_error()
@staticmethod
def _close_db_session(db_session):
"""Close database connection"""
if db_session:
db_session.close()
Functions
-
Raise HTTP 503 - Internal server error
Expand source code
def handle_stag_unavailable_error(): """Raise HTTP 503 - Internal server error""" abort(503, message="Request to STAG API failed.") def internal_server_error()-
Raise HTTP 500 - Internal server error
Expand source code
def internal_server_error(): """Raise HTTP 500 - Internal server error""" abort(500, message="Internal server error.") def verify(username, password)-
Verify incoming request credentials
Args
username:str- user's login
password:str- user's password
Returns
bool- True if user and password are ok otherwise False
Expand source code
@auth.verify_password def verify(username, password): """Verify incoming request credentials Args: username (str): user's login password (str): user's password Returns: bool: True if user and password are ok otherwise False """ if not (username and password): return False return CREDENTIALS.get(username) == password
Classes
class Image-
Represents an abstract RESTful resource. Concrete resources should extend from this class and expose methods for each supported HTTP method. If a resource is invoked with an unsupported HTTP method, the API will return a response with status 405 Method Not Allowed. Otherwise the appropriate method is called and passed all arguments from the url rule used when adding the resource to an Api instance. See :meth:
~flask_restful.Api.add_resourcefor details.Expand source code
class Image(Resource): @auth.login_required def get(self, image_id): """GET /images/<image_id> HTTP method Args: image_id (int): ID reference of question's picture """ try: db_session = None try: db = Database() db_session = db.get_db_session() except OperationalError: logger.exception(msg="Problem with connection to database") self._close_db_session(db_session) abort(500, message="Can not connect to database.") try: image = db.find_question_image(db_session, image_id) except LookupError: self._close_db_session(db_session) abort(404, message="Invalid image ID.") if not image.picture: abort(404, message="This question does not have image.") self._close_db_session(db_session) return {"image": (base64.b64encode(image.picture)).decode("utf-8")} except HTTPException: raise except Exception as err: logger.exception(err) raise internal_server_error() @staticmethod def _close_db_session(db_session): """Close database connection""" if db_session: db_session.close()Ancestors
- flask_restful.Resource
- flask.views.MethodView
- flask.views.View
Class variables
var methods
Methods
def get(self, image_id)-
GET /images/
HTTP method Args
image_id:int- ID reference of question's picture
Expand source code
@auth.login_required def get(self, image_id): """GET /images/<image_id> HTTP method Args: image_id (int): ID reference of question's picture """ try: db_session = None try: db = Database() db_session = db.get_db_session() except OperationalError: logger.exception(msg="Problem with connection to database") self._close_db_session(db_session) abort(500, message="Can not connect to database.") try: image = db.find_question_image(db_session, image_id) except LookupError: self._close_db_session(db_session) abort(404, message="Invalid image ID.") if not image.picture: abort(404, message="This question does not have image.") self._close_db_session(db_session) return {"image": (base64.b64encode(image.picture)).decode("utf-8")} except HTTPException: raise except Exception as err: logger.exception(err) raise internal_server_error()
class Question-
Represents an abstract RESTful resource. Concrete resources should extend from this class and expose methods for each supported HTTP method. If a resource is invoked with an unsupported HTTP method, the API will return a response with status 405 Method Not Allowed. Otherwise the appropriate method is called and passed all arguments from the url rule used when adding the resource to an Api instance. See :meth:
~flask_restful.Api.add_resourcefor details.Expand source code
class Question(Resource): @auth.login_required def get(self): """GET /questions HTTP method""" try: args = self._validate_request_params() db_session = None try: db = Database() db_session = db.get_db_session() except OperationalError: logger.exception(msg="Problem with connection to database") self._close_db_session(db_session) abort(500, message="Can not connect to database.") device = db.device_by_id(db_session, args["device_id"]) if not device: abort(400, message="This device id is not registered in database") # Get assigned study programmes to a device device_permissions = [d.study_code for d in device.study_programmes] db.store_access_log(db_session, args["card_number"], args["device_id"]) stag = StagCommunicator() try: studies = stag.get_active_studies(card_number=args["card_number"]) study_codes = stag.get_study_codes_for_active_studies(studies) self.evaluate_user_device_permission( reference_codes=device_permissions, study_codes=study_codes ) # Get the subjects for the first active study completed_subjects = stag.get_completed_subjects_for_study( study=studies[0], actual_year=datetime.today().year ) except NoActiveStudyError: logger.info(f"No active study for card_number: {args['card_number']}") self._close_db_session(db_session) abort( 404, message="No active study found.", error_code="NO_ACTIVE_STUDY" ) except StagUnavailableError: logger.exception(msg="Can not connect establish connection to STAG.") self._close_db_session(db_session) handle_stag_unavailable_error() number_of_questions = ( # set requested number of questions args.get("question_count") if args.get("question_count") else DEFAULT_NUMBER_OF_QUESTIONS ) subjects = self._choose_subjects(completed_subjects, number_of_questions) questions = db.select_questions( db_session=db_session, subjects=subjects, requested_number_of_questions=number_of_questions, ) questions = [self._question_to_dict(q) for q in questions] self._close_db_session(db_session) return { "questions": questions, "student_name": self._student_name(studies[0]), } except HTTPException: raise except Exception as err: logger.exception(err) internal_server_error() def _validate_request_params(self): """HTTP params parser and validator Returns: dict: Incoming request parameters Raises: HTTPException: HTTP 400 bad requests on invalid params """ arg_parser = reqparse.RequestParser() arg_parser.add_argument( "card_number", type=str, required=True, location="args", ) arg_parser.add_argument("device_id", type=int, required=True, location="args") arg_parser.add_argument( "question_count", type=int, required=False, location="args" ) args = arg_parser.parse_args(strict=True) self._validate_card_number(card_number=args["card_number"]) self._validate_device_id(device_id=args["device_id"]) self._validate_questions_count(question_count=args.get("question_count")) return args def evaluate_user_device_permission(self, reference_codes, study_codes): """Evaluate if user has access to this device Args: reference_codes list(str): list of allowed codes study_codes list(str): list of student codes Raises: HTTPException: HTTP 403 Forbidden - access not allowed """ if not self._is_study_code_in_reference(study_codes, reference_codes): abort(403, message="This student does not have access to this device") @staticmethod def _is_study_code_in_reference(study_codes, reference_codes): """Test if some of student codes is in reference Args: reference_codes list(str): list of allowed codes study_codes list(str): list of student codes Returns: True if reference contains some student code otherwise False """ if DEFAULT_PERMISSION in reference_codes: return True for study_code in study_codes: if study_code in reference_codes: return True return False @staticmethod def _student_name(study_info): """Format student name based on info from Stag Args: study_info (dict): Info about student from Stag Returns: str: student's name """ if study_info["titulPred"]: name = f"{study_info['titulPred']} " else: name = "" name += f"{(study_info['jmeno']).upper()} {(study_info['prijmeni']).upper()}" if study_info["titulZa"]: name += f", {study_info['titulZa']}" return name @staticmethod def _close_db_session(db_session): """Close database connection""" if db_session: db_session.close() @staticmethod def _choose_subjects(completed_subjects, number_of_questions): """Choose random subjects from completed study Args: completed_subjects (list): student's absolved subjects number_of_questions (int): number of subjects to choose Returns: list: choose subjects """ if not completed_subjects: return [] if len(completed_subjects) < number_of_questions: return completed_subjects return random.sample(completed_subjects, number_of_questions) @staticmethod def _validate_card_number(card_number): """Test if card number matches expected format Args: card_number (str): Student's card number Raises: HTTPException: HTTP 400 Bad request on invalid card number """ if not re.match(r"^[A-Z0-9]{8}$", card_number): abort(400, message="Invalid card_number format.") @staticmethod def _validate_questions_count(question_count): """Test if requested number of questions is in pre-set range Args: question_count (int): number of questions requested by client Raises: HTTPException: HTTP 400 Bad request on invalid number """ if question_count is not None: if question_count <= 0 or question_count > MAX_NUMBER_OF_QUESTIONS: abort( 400, message=f"Invalid question_count. Value must be in " f"range 1-{MAX_NUMBER_OF_QUESTIONS}.", ) @staticmethod def _validate_device_id(device_id): """Test if device id is not negative Args: device_id (int): id of device Raises: HTTPException: HTTP 400 Bad request on invalid number """ if device_id < 1: abort(400, message=f"Invalid device_id. Value must be positive.") @staticmethod def _question_to_dict(question_entity): """Transform question to output format Args: question_entity: DB question class entity Returns: dict: Response (json) format of question """ return { "question": question_entity.question, "department_shortcut": question_entity.department_shortcut, "subject_name": question_entity.subject.name, "right_answer": question_entity.right_answer, "wrong_answer_1": question_entity.wrong_answer_1, "wrong_answer_2": question_entity.wrong_answer_2, "wrong_answer_3": question_entity.wrong_answer_3, "picture_id": question_entity.id if question_entity.picture else None, }Ancestors
- flask_restful.Resource
- flask.views.MethodView
- flask.views.View
Class variables
var methods
Methods
def evaluate_user_device_permission(self, reference_codes, study_codes)-
Evaluate if user has access to this device
Args
reference_codes list(str): list of allowed codes study_codes list(str): list of student codes
Raises
HTTPException- HTTP 403 Forbidden - access not allowed
Expand source code
def evaluate_user_device_permission(self, reference_codes, study_codes): """Evaluate if user has access to this device Args: reference_codes list(str): list of allowed codes study_codes list(str): list of student codes Raises: HTTPException: HTTP 403 Forbidden - access not allowed """ if not self._is_study_code_in_reference(study_codes, reference_codes): abort(403, message="This student does not have access to this device") def get(self)-
GET /questions HTTP method
Expand source code
@auth.login_required def get(self): """GET /questions HTTP method""" try: args = self._validate_request_params() db_session = None try: db = Database() db_session = db.get_db_session() except OperationalError: logger.exception(msg="Problem with connection to database") self._close_db_session(db_session) abort(500, message="Can not connect to database.") device = db.device_by_id(db_session, args["device_id"]) if not device: abort(400, message="This device id is not registered in database") # Get assigned study programmes to a device device_permissions = [d.study_code for d in device.study_programmes] db.store_access_log(db_session, args["card_number"], args["device_id"]) stag = StagCommunicator() try: studies = stag.get_active_studies(card_number=args["card_number"]) study_codes = stag.get_study_codes_for_active_studies(studies) self.evaluate_user_device_permission( reference_codes=device_permissions, study_codes=study_codes ) # Get the subjects for the first active study completed_subjects = stag.get_completed_subjects_for_study( study=studies[0], actual_year=datetime.today().year ) except NoActiveStudyError: logger.info(f"No active study for card_number: {args['card_number']}") self._close_db_session(db_session) abort( 404, message="No active study found.", error_code="NO_ACTIVE_STUDY" ) except StagUnavailableError: logger.exception(msg="Can not connect establish connection to STAG.") self._close_db_session(db_session) handle_stag_unavailable_error() number_of_questions = ( # set requested number of questions args.get("question_count") if args.get("question_count") else DEFAULT_NUMBER_OF_QUESTIONS ) subjects = self._choose_subjects(completed_subjects, number_of_questions) questions = db.select_questions( db_session=db_session, subjects=subjects, requested_number_of_questions=number_of_questions, ) questions = [self._question_to_dict(q) for q in questions] self._close_db_session(db_session) return { "questions": questions, "student_name": self._student_name(studies[0]), } except HTTPException: raise except Exception as err: logger.exception(err) internal_server_error()